package model.handler.databaseHandling;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Date;

import model.Control;
import model.data.tableModel.TableControl;
import model.data.tableModel.person.Staff;
import model.data.tableModel.sale.Salesday;
import model.invoicing.DateHandler;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import enums.Message;
import enums.Status;

/**
 * Checks and verifies the login of a user and writes the access to the
 * database.
 * <p>
 * Is implemented as a Singleton to ensure that there is only one LoginHandler
 * at each time.
 * 
 * @version %I%, %G%
 */
public final class LogInOutHandler {

	private final static Logger log = LogManager.getLogger("ERROR");
	private final static Logger userLog = LogManager.getLogger("USER");
	private final static Logger debug = LogManager.getLogger("DEBUG");
	
	private final Control control;
	private static LogInOutHandler instance;
	private final TableControl tableControl = TableControl.getInstance();
	private boolean isAdmin;

	/**
	 * Private constructor because it is implemented as Singleton.
	 * 
	 * @param control Reference to the Controller due to the MVC-Pattern,
	 *            mustn't be null
	 */
	private LogInOutHandler(Control control) {
		assert control != null;

		isAdmin = false;
		this.control = control;
	}

	/**
	 * Gets the only instance of the as Singleton implemented LoginHandler.
	 * 
	 * @param controller Reference to the controller due to the MVC-Pattern
	 * @return the instance of this class
	 */
	public static LogInOutHandler getInstance(final Control control) {

		if (instance == null) {
			instance = new LogInOutHandler(control);
		}
		return instance;
	}

	/**
	 * Tries to log a user in.
	 * 
	 * @param nick Nick should be logged in
	 * @param password Password that was entered
	 * @param cash Cash in the cash box
	 * @param coins Number of washing tokens available
	 * @throws IllegalArgumentException if values below zero are entered,
	 *             because there cannot be a negative number of cash and coins
	 * @throws NumberFormatException if other than int values were entered for
	 *             cash and coins
	 */
	public void tryLogin(final String nick, final String password, final String cash, final String coins) {
		assert nick != null && !"".equals(nick);
		assert password != null && !"".equals(password);
		assert cash != null && !"".equals(cash);
		assert coins != null && !"".equals(coins);

		Staff staff = TableControl.getInstance().getStaffMap().get(nick);
		if (staff == null) {
			control.loginFailed(Message.WRONGUSER);
			userLog.warn("Login failed because wrong nick: " + nick + " was entered.");
		} else {
			if (checkEquality(password, staff.getPassword(), staff.getSalt())) {// If entered password is correct
				
				if (!staff.getStatus().equals(Status.ISADMIN)) {// If the user is not the admin
					if (cash != null && coins != null && !cash.equals("") && !coins.equals("")) {// If all information required were entered

						double numberCash = Double.valueOf(cash);
						int numberCoins = Integer.valueOf(coins);
						assert numberCash >= 0 && numberCoins >= 0;

						// Create new salesday
						tableControl.setActualSalesday(new Salesday(DateHandler.formatNormalDate(new Date()), tableControl.getStaffMap().get(nick), Status.ACTIVE, numberCoins, numberCash));
						control.loginSuccess(isAdmin); // inform the view that the
												// login was successful

						userLog.info(nick + " logged in successfully");
					} else {
						control.loginFailed(Message.MSGMISSINGINFO);
						userLog.warn(nick + ": Login failed because not all required information were entered.");
					}
				} else {
					isAdmin = true;
					control.loginSuccess(isAdmin);
					userLog.info(nick + " logged in successfully");
				}
			} else {
				control.loginFailed(Message.WRONGPW);
				userLog.warn(nick + ": Login failed because a wrong password was entered.");
			}
		}
	}

	/**
	 * Tries to log the user out
	 * 
	 * @param cash count of cash after the end of the salesday
	 * @param coins count of coins after the end of the salesday
	 * @throw IllegalArgumentException if negative values were entered
	 */
	public void tryLogout(final String cash, final String coins) {

		assert cash != null && !"".equals(cash);
		assert coins != null && !"".equals(coins);

		// If all required information were entered
		if (cash != null && coins != null && !cash.equals("") && !coins.equals("")) {

			double numberCash = Double.valueOf(cash);
			int numberCoins = Integer.valueOf(coins);
			// Shouldn't happen because of validation of the user input at the
			// view
			assert numberCash >= 0 && numberCoins >= 0;
			if (!isAdmin) {
				tableControl.getActualSalesday().setCashEnd(numberCash);
				tableControl.getActualSalesday().setCoinEnd(numberCoins);
				tableControl.writeToDatabase();
			}
			control.logOutSuccess();
			

			userLog.info("Logout successful");
		} else {
			control.logoutFailed(Message.MSGMISSINGINFO);
			userLog.warn("Logout failed: " + Message.MSGMISSINGINFO.toString());
		}
	}
	
	/**
	 * Checks if the entered password is equal to the stored password of the given nick
	 * @param enteredPassword
	 * @param nick
	 * @return true if the pws are equal | false if not
	 */
	public boolean checkEquality(String enteredPassword, String storedHashedPW, String salt) {
		
		String hashed = getHash(enteredPassword, salt);
		
		return hashed.equals(storedHashedPW);
	}
	
	/**
	 * Sets a new password for a staff
	 * @param staff
	 * @param newPw
	 * @return true if successful | false if not
	 */
	public boolean setNewPw(Staff staff, String newPw) {
		
		assert staff != null && !"".equals(staff);
		assert newPw != null && !"".equals(newPw);
		
		String newSalt = getSalt();
		
		return staff.setPassword(getHash(newPw, newSalt), newSalt);
	}

	/**
	 * Generates a hash out of a String.
	 * <p>
	 * Uses the SHA-256 hashing algorithm. Is invoked to hash a password.
	 * 
	 * @param passwordToHash Password of which the hash should be created
	 * @param salt Salt in order to improve the hash
	 * @return Hash of the given String
	 */
	private static String getHash(String passwordToHash, String salt) {

		assert passwordToHash != null && !"".equals(passwordToHash);
		assert salt != null && !"".equals(salt);

		String generatedPassword = null;
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-256");
			md.update(salt.getBytes());
			byte[] bytes = md.digest(passwordToHash.getBytes());
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < bytes.length; i++) {
				sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
			}
			generatedPassword = sb.toString();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			log.error(e.getMessage());
		}
		debug.debug("New hash created");
		return new String(generatedPassword);
	}

	/**
	 * Generates a salt in order to improve the hashing.
	 * 
	 * @return Salt use to improve the security of the hash
	 */
	private static String getSalt() {

		SecureRandom sr;
		byte[] salt = new byte[16];
		try {
			sr = SecureRandom.getInstance("SHA1PRNG");
			sr.nextBytes(salt);
		} catch (NoSuchAlgorithmException e) {
			log.error(e.getMessage());
			e.printStackTrace();
		}
		debug.debug("New salt created");
		return new String(salt.toString());
	}
}
